package goinfo

import (
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/gookit/goutil/x/basefn"
)

// some commonly consts
var (
	DefStackLen = 10000
	MaxStackLen = 100000
)

// GetCallStacks stacks is a wrapper for runtime.
// If all is true, Stack that attempts to recover the data for all goroutines.
//
// from glog package
func GetCallStacks(all bool) []byte {
	// We don't know how big the traces are, so grow a few times if they don't fit.
	// Start large, though.
	n := DefStackLen
	if all {
		n = MaxStackLen
	}

	// 4<<10 // 4 KB should be enough
	var trace []byte
	for i := 0; i < 10; i++ {
		trace = make([]byte, n)
		bts := runtime.Stack(trace, all)
		if bts < len(trace) {
			return trace[:bts]
		}
		n *= 2
	}
	return trace
}

// GetCallerInfo get caller func name and with base filename and line.
//
// returns:
//
//	github.com/gookit/goutil/x/goinfo_test.someFunc2(),stack_test.go:26
func GetCallerInfo(skip int) string {
	skip++ // ignore current func
	cs := GetCallersInfo(skip, skip+1)
	return basefn.FirstOr(cs, "")
}

// SimpleCallersInfo returns an array of strings containing
// the func name, file and line number of each stack frame leading.
func SimpleCallersInfo(skip, num int) []string {
	skip++ // ignore current func
	return GetCallersInfo(skip, skip+num)
}

// GetCallersInfo returns an array of strings containing
// the func name, file and line number of each stack frame leading.
//
// NOTICE: max should > skip
func GetCallersInfo(skip, max int) []string {
	var (
		pc         uintptr
		ok         bool
		line       int
		file, name string
	)

	callers := make([]string, 0, max-skip)
	for i := skip; i < max; i++ {
		pc, file, line, ok = runtime.Caller(i)
		if !ok {
			// The breaks below failed to terminate the loop, and we ran off the
			// end of the call stack.
			break
		}

		// This is a huge edge case, but it will panic if this is the case
		if file == "<autogenerated>" {
			break
		}

		fc := runtime.FuncForPC(pc)
		if fc == nil {
			break
		}

		if strings.ContainsRune(file, '/') {
			name = fc.Name()
			file = filepath.Base(file)
			// eg: github.com/gookit/goutil/x/goinfo_test.someFunc2(),stack_test.go:26
			callers = append(callers, name+"(),"+file+":"+strconv.Itoa(line))
		}

		// Drop the package
		// segments := strings.Split(name, ".")
		// name = segments[len(segments)-1]
	}

	return callers
}

// CallerInfo struct
type CallerInfo struct {
	PC   uintptr
	Fc   *runtime.Func
	File string
	Line int
}

// String convert
func (ci *CallerInfo) String() string {
	return ci.File + ":" + strconv.Itoa(ci.Line)
}

// CallerFilterFunc type
type CallerFilterFunc func(file string, fc *runtime.Func) bool

// CallersInfos returns an array of the CallerInfo, can with filters
//
// Usage:
//
//	cs := sysutil.CallersInfos(3, 2)
//	for _, ci := range cs {
//		fc := runtime.FuncForPC(pc)
//		// maybe need check fc = nil
//		fnName = fc.Name()
//	}
func CallersInfos(skip, num int, filters ...CallerFilterFunc) []*CallerInfo {
	filterLn := len(filters)
	callers := make([]*CallerInfo, 0, num)
	for i := skip; i < skip+num; i++ {
		pc, file, line, ok := runtime.Caller(i)
		if !ok {
			// The breaks below failed to terminate the loop, and we ran off the
			// end of the call stack.
			break
		}

		fc := runtime.FuncForPC(pc)
		if fc == nil {
			continue
		}

		if filterLn > 0 && filters[0] != nil {
			// filter - return false for skip
			if !filters[0](file, fc) {
				continue
			}
		}

		// collecting
		callers = append(callers, &CallerInfo{
			PC:   pc,
			Fc:   fc,
			File: file,
			Line: line,
		})
	}

	return callers
}
